Taiwan travel-頁面功能、component、folder、router、UX 設計思維 (Part1)

2022-08-31 Wed

本文為製作 Taiwan travel 的個人想法,如果有其他建議歡迎與我交流🙂

總共分為 4 個 part 如下:

  • Taiwan travel-頁面功能、component、folder、router、UX 設計思維 (Part1)
  • Taiwan travel-css layout、url 設計、module design、readability(Part2)
  • Taiwan travel-issue and solution(Part3)
  • Taiwan travel-可改善部分、筆記、其他知識點 (Part4)

以下為 Part1 的大綱

  • 頁面功能
    • 首頁
    • 搜尋頁面
    • 詳細頁面
  • 使用技術
  • 資料夾的拆分方式
  • component 的拆分思維
  • route 設計的思維
  • 使用者體驗

頁面功能

以下將會講解整個網站可以達到的功能。

首頁

  • 點選右方可以得到更多該類型的資訊

  • 點選目的地可選擇城市
    • 未選擇城市預設會全部搜尋

  • 輸入內容至搜尋關鍵字 input 欄位可以得到關鍵字的搜尋結果

  • 點選精選主題將會得到該主題內容

  • 點選任意地點 (卡片) 可以得到該地點的詳細頁面

搜尋頁面

搜尋結果可得到地址和營業時間 點擊Pagination可跳轉該頁、上、下頁和首頁及最後頁

可以分享搜尋結果給其他人能得到相同的資訊

詳細頁面

  • 開源地圖,可拖放得知位置資訊

  • 下方更多景點可以得到隨機產出三個景點

使用技術

  • axios
  • dayjs
  • leaflet
  • qs
  • react-leaflet
  • react-redux
  • redux-persist
  • fontAwesome
  • react-toolkit

資料夾的拆分方式

  • API:API 路由的地方
  • assets:圖片或資料的地方
  • component:元件的地方
  • hook:Custom hook 的地方
  • store:Redux 設定的地方
  • styles:被 mixin 或 global 的 scss 的地方
資料夾名稱 內容物
API API 路由
assets 圖片或資料
component 元件
hook Custom hook
store Redux 設定
styles 被 mixin 或 global 的 scss

下方為資料夾實際結構

├─API
├─assets
│  └─images
│      └─best_theme
├─component
│  ├─Aside
│  ├─Banner
│  ├─Card
│  ├─Footer
│  ├─Header
│  ├─Logo
│  ├─Pagination
│  ├─Sidebar
│  ├─ThemeCardContentByVisitType
│  ├─ThemeSection
│  └─ThemeTitle
├─hook
├─pages
│  ├─Detail
│  ├─HomePage
│  ├─NotFound
│  └─Search
├─store
└─styles

component 的拆分思維

考量到 react 使用Composition的概念,拆分 component 其考量到的部分包含複用性、將大問題拆解成小問題、讓單一程式碼複雜度降低,這邊拆分的原件如圖

使用物件和方法產生 component

比較值得提的拆分構想是 ThemeCardVisitType,由於不同類型卡片資訊、像是熱門景點、觀光活動、美食品嘗、住宿推薦,其卡片內容組成是圖像 + 卡片內容資訊,而卡片內容資訊的部分有的是地址 + 開放時間、有的是地址 + 活動時間、有的是地址 + 電話 如下列的圖片

加上由 API 回傳回來的 data 會根據不同的類型而不同,例如觀光類的 key 會叫做 ScenicSpotName、美食類的 data 的 key 會是 RestaurantName,還有要顯示的圖片也會根據類型的不同而不同 因此建置了一個ThemeCardContentByVisitType的物件來產生其 Component 如下圖

由於卡片內容 (CardContent) 的 jsx 的部分並不多,也避免在撰寫Card 元件的時候暴露太多邏輯,轉而改用建構成物件型式,透過接收 children 的方式,這樣做的好處是當我們瀏覽卡片元件區段的程式碼,讓 Card 元件就處理 Card 的事情 (把 Card 作為容器和觸發事件用),例如點擊卡片元件事件的頁面跳轉邏輯、錯誤處理和接收 children 將其作為 UI 渲染而已,如下列程式碼

備註:如果不將卡片內容拆分出小元件的話,可能在進到卡片元件的邏輯時,需要使用多個 if 或者 switch 判別是哪種類型的卡片內容元件,使得 Card 元件暴露太多邏輯。

const Card = ({ placeDatum, visitType, children }) => {
const { card, wrap_img } = styles;
const navigate = useNavigate();
const handleError = (e) => {
    e.currentTarget.src = banner;
}
const clickCardHandle = () => {
    const id = placeDatum[`${visitType}ID`]
    let url = `${visitType}?$filter=${visitType}ID eq '${id}'`;
    navigate(`/detail/${url}`);
}

// card 不要共用到太複雜,而是別人給他什麼 他就渲染成什麼

return (
    <div className={card} onClick={clickCardHandle}>
        <div className={wrap_img}>
            <img src={placeDatum.Picture.PictureUrl1 || banner} onError={(error) => handleError(error)} alt="" />
        </div>
        <p>{placeDatum[`${visitType}Name`]}</p>
        {children}
    </div>
)
}

在一些需要 Card 元件的地方僅需要像一般的 html 的 tag 使用方式透過 children 方式傳遞給 Card,其邏輯部分已經在ThemeCardContentByVisitType物件處理了。

<div className={wrap_card}>
  {
    randomPlace.map(
        (place, index) =>
        (
          <Card key={index} visitType={visitType} placeDatum={place}>
              <CardContent placeDatum={place} />
          </Card>
        )
    )
  }
</div>

另外 Detail 頁面有部分需要顯示的畫面與卡片的內容重疊,因此也可以複用ThemeCardContentByVisitType物件

使用 children 時機 - 如同看 html tag 般的閱讀性

由於 detail 頁面與 search 頁面都有 header,但其內容差別一個是 render 標題,另一個 render 是 Banner 元件,因此使用讓 children 裡面帶入的內容用起來像是在看 html tag 擁有階層關係的 code

觀看程式碼的時候如下 在 HomePage 頁面

<Header menuValue={menuValue} >
  <Banner />
</Header>

在 Search 頁面

<Header >
  <h2 className={`${title_type} ${menuValue ? display_none : display_block}`}>
    {title ||= "搜尋結果"}
  </h2>
</Header>

簡化頁面檔暴露的 component

另外主要頁面檔的 jsx 會盡量以類似 HTML5 的語意化標籤 (Semantic Elements) 的命名的元件為主 以 NotFound 頁面的 jsx 為例就會僅有 Aside、Header、Banner 和 Footer 的 component,如果想了解 Aside 內容包含些什麼的話,再去 Aside Component 觀看其程式碼。

<div className={container}>
  <Aside
      menuValue={menuValue}
      menuValueFunction={menuValueFunction}
  />
  <Header menuValue={menuValue} >
    <Banner />
  </Header>
  <article className={`${article} ${menuValue ? display_none : display_block}`}>
      找不到該分頁
  </article>
  <Footer menuValue={menuValue} />
</div >

父層可查看 component 的傳入的 props 內容

在首頁的部分有熱門景點、觀光活動、美食品嘗、住宿推薦使用 ThemeSection 元件透過 prop 可以看出傳入的資料流是什麼內容 以首頁的程式碼為例如下

<article className={`${article} ${menuValue ? display_none : display_block}`}>
  <ThemeSection
      title="熱門景點"
      visitType="ScenicSpot"
      placeData={placeDataScenicSpot}
      CardContent={ScenicSpot}
      moreQuery={SCENICSPOT_MORE_QUERY}
  />
  <ThemeSection
      title="觀光活動"
      visitType="Activity"
      placeData={placeDataActivity}
      CardContent={Activity}
      moreQuery={ACTIVITY_MORE_QUERY}
  />
  <ThemeSection
      title="美食品嘗"
      visitType="Restaurant"
      placeData={placeDataRestaurant}
      CardContent={Restaurant}
      moreQuery={RESTAURANT_MORE_QUERY}
  />
  <ThemeSection
      title="住宿推薦"
      visitType="Hotel"
      placeData={placeDataHotel}
      CardContent={Hotel}
      moreQuery={HOTEL_MORE_QUERY}
  />
</article>

適時使用 composition 概念

在 ThemeSection 裡面再次使用React 的核心概念 Composition,藉由 ThemeTitle 和 Card 去構成 Component 程式碼如下

<ThemeTitle visitType={visitType} title={title} moreQuery={moreQuery} />
<div className={wrap_card}>
  {placeData?.map(
    (place, index) =>
      (
        <Card 
          visitType={visitType} 
          key={index} 
          placeDatum={place}>
          <CardContent placeDatum={place} />
        </Card>
      )
    )
  }
</div>

route 設計的思維

<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/search/:visitType/:city/:page" element={<Search />} />
  <Route path="/detail/:visitType" element={<Detail />} />
  <Route path="*" element={<NotFound />} />
</Routes>

共用 search route

由於設計稿畫面呈現 SideBar 搜尋後和首頁點選更多景點的畫面差異不大,所以在 react-route 切分出/search

visitType、page 參數設定與 TDX API 類似

其 visitType 和 City 參數也設定成和 TDX 的 API Route(如上圖) 類似,visitType 表示觀光種類ScenicSpot、Restaurant、Hotel或者Activity這四種 API 其中一種,city 表示某個縣市,如果想要搜尋全台參數則是帶入 all(在後面資料抽離的部分會顯示。

page 參數

當搜尋結果過多的時候將會進行分頁,因此添加 page 參數進行頁面劃分。

詳細頁面 route

點擊任何卡片頁面將會導向/Detail的 Route

not found 頁面

預設除了上述自定義 Route 以外的 path 就會導向 NotFound 頁面。

使用者體驗

雖然當初再看設計稿的時候有些並非是設計師具有詳細說明的地方,但是畢竟前端是要製作畫面有關的工程,也是面對使用者的第一道線,因此這邊會簡單說明製作了那些使用者體驗。

使用 hover 和 active 增加或改變顏色。

精選主題的 Icon

在滑鼠的游標 hover 和 active 的時候,添加 border,在 active 的時候顏色改略為淡色,增加回饋感。

上下頁切換按鈕

在 pagination 的部分 hover 改變 background 的顏色,一樣在點擊的時候 (active) 使用淡色系增加回饋感。

使用 scale 微放大 icon

Logo 點擊回首頁

使用scale,微放大 Logo 按鈕表示可供點擊的提示。

FontAwesome 回上一頁

使用fontAwesome製作回上一頁的按鈕,一樣在hover的時候使用scale語法微放大表示可供點擊。

更多景點的連結與卡片

a {
    color: variable.$primary_color1;
    position: relative;
    padding: 10px 0px;
    white-space: nowrap;
    &:after {
      content: "";
      position: absolute;
      height: 5px;
      width: 100%;
      left: 0;
      bottom: 0;
      background: variable.$primary_color1;
      transform: scaleX(0);
      transform-origin: bottom left;
      border-radius: 4px;
    }
    &:hover:after {
      transition: transform 0.5s;
      transform: scaleX(1);
    }
  }

使用偽元素技巧製作下底線,原先的樣貌是scaleX(0)在 hover 的時候會變成scaleX(1),使用transform優點是可以透過tansform-origin的屬性決定延伸的方向,以這裡的語法為例是在文字的下方由左至右延伸,另外添加一點點border-radius使線條帶點圓潤感。

dropdown 的減少突兀感,選擇城市後自動收回

這邊使用max-height從 0 漸變到其設定的高度,icon 的部分使用rotate暗示可以再次點擊收回下拉式選單,在使用者體驗上面也透過動畫減少突兀感,另外點擊選擇的城市後則會自動收回以便使用者順勢搜尋關鍵字。如下列兩張圖。